package org.geometerplus.zlibrary.core.filesystem.ceb;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.List;


public class CEBDecompressor
{
	// ////////////////////////////////////////文件格式描述区段相对偏移定义
	/** 文件类型:ceb */
	private static String FILE_TYPE_CEB             = "CEB";
	
	/** 文件类型:META-INF */
	private static String FILE_TYPE_MI              = "META-INF";
	
	/** 文件类型:DATA */
	private static String FILE_TYPE_DATA            = "oebps";
	
	/** 文件格式描述区段长度,固定为132字节 */
	private static int    FILE_FORMAT_LENGTH        = 132;
	
	/** 起始位:文件类型标识,CEB文件标识为XCEB,4字节 */
	private static int    FILE_TYPE                 = 0x0C;
	
	/** 起始位:电子书类型标识,4字节 */
	private static int    BOOK_TYPE                 = 0x14;
	
	/** 起始位:META-INF文件区段起始位置,4字节 */
	private static int    META_INF_BEGIN            = 0x20;
	
	/** 起始位:META-INF文件区段长度,4字节 */
	private static int    META_INF_LENGTH           = 0x24;
	
	/** 起始位:数据区段索引表起始位置,4字节 */
	private static int    DATA_INDEX_BEGIN          = 0x28;
	
	/** 起始位:数据区段索引表长度,4字节 */
	private static int    DATA_INDEX_LENGTH         = 0x2C;
	
	/** 起始位:AES密钥种子,使用8字节 */
	private static int    AES_SEED_BEGIN            = 0x64;
	
	// ///////////////////////////////////////////////////////////////
	// ///////////////////////////////////////META-INF文件区段相对偏移定义
	/** 起始位:META-INF文件区段内文件个数,2字节 */
	private static int    META_INF_FILE_NUM         = 0x00;
	
	/** 起始位:META-INF文件区段：文件映射表长度,2字节 */
	private static int    FILE_MAPPING_LENGTH       = 0x02;
	
	// ///////////////////////////////////////////////////////////////
	// //////////////////////////////////////////数据区段索引表相对偏移定义
	/** 起始位:数据区段总个数,2字节 */
	private static int    DATA_AREA_NUM             = 0x00;
	
	/** 起始位:数据区段索引表的区段映射表长度,2字节 */
	private static int    DATA_INDEX_MAPPING_LENGTH = 0x02;
	
	// ///////////////////////////////////////////////////////////////
	/** 传入的电子书类型ceb/opf */
	private String        bookFileType              = "CEB";
	
	byte[]                aesKey                    = null;
	
	public CEBDecompressor() {
	}
	
	/** CEB文件解包程序,大内存使用版(PC ver.) */
	public void DecompressCEBPackage(String filePath, CebFile targetFile) throws Exception {
		// ceb解包存放文件目录
		String folder = filePath.substring(0, filePath.lastIndexOf(".")) + "/";
		File f = new File(folder);
		if (!f.exists()) f.mkdirs();
		else f.delete();
		
		// 文件读入
		FileInputStream fis = new FileInputStream(filePath);
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		byte[] buffer = new byte[1024];
		int len = 0;
		while ((len = fis.read(buffer)) > 0)
		{
			bos.write(buffer, 0, len);
		}
		byte[] ceb = bos.toByteArray();
		fis.close();
		bos.close();
		
		int curPos = 0;// 游标
		byte[] temp = new byte[4];
		// 检查文件类型
		curPos = FILE_TYPE;
		System.arraycopy(ceb, FILE_TYPE, temp, 0, 4);
		temp = new byte[2];
		System.arraycopy(ceb, BOOK_TYPE, temp, 0, 2);
		String numSort = String.valueOf(bytesToInt(temp));// 一级分类
		temp = new byte[2];
		System.arraycopy(ceb, BOOK_TYPE + 2, temp, 0, 2);
		String numSecSort = String.valueOf(bytesToInt(temp));// 二级分类
		temp = new byte[8];
		System.arraycopy(ceb, AES_SEED_BEGIN, temp, 0, 8);
		aesKey = changeBytes(temp);
		// ////////////////////////////////////////////////把密钥输出为文件
		//writeFile(folder + "AES密钥", aesKey);
		// ////////////////////////////////////////////////////////////////////////////
		// META-INF区内文件引用数据获取
		temp = new byte[4];
		curPos = META_INF_BEGIN;
		System.arraycopy(ceb, curPos, temp, 0, 4);
		curPos = bytesToInt(temp);// META-INF区起始位
		temp = new byte[2];
		System.arraycopy(ceb, curPos, temp, 0, 2);
		int miFileNum = bytesToInt(temp);// META-INF文件区段内文件个数
		
		List<Object[]> miFileList = new ArrayList<Object[]>();// 存储[段内id,文件是否压缩,压缩类型,文件起始位置(绝对偏移),文件长度,文件名称]的列表
		
		curPos += 4;// 第一个文件起始位置信息存储开始位
		int miAllFileLength = 0;
		for (int i = 0; i < miFileNum; i++)
		{
			Object[] oMiFile = new Object[7];// [段内id,文件是否压缩,压缩类型,文件起始位置(绝对偏移),文件长度,文件名称]
			temp = new byte[2];
			System.arraycopy(ceb, curPos, temp, 0, 2);
			oMiFile[0] = bytesToInt(temp);// 段内id
			curPos += 2;// 文件压缩标识开始位
			temp = new byte[2];
			System.arraycopy(ceb, curPos, temp, 0, 2);
			oMiFile[1] = (new Integer(temp[0])) == 0 ? false : true;// 文件是否压缩
			oMiFile[2] = new Integer(temp[1]);// 文件压缩类型，默认为0
			curPos += 2;// 文件起始位置开始位
			temp = new byte[4];
			System.arraycopy(ceb, curPos, temp, 0, 4);
			oMiFile[3] = FILE_FORMAT_LENGTH + bytesToInt(temp);
			curPos += 4;// 文件长度开始位
			temp = new byte[4];
			System.arraycopy(ceb, curPos, temp, 0, 4);
			oMiFile[4] = bytesToInt(temp);
			miAllFileLength += bytesToInt(temp);
			curPos += 4;// 文件名称长度开始位
			temp = new byte[2];
			System.arraycopy(ceb, curPos, temp, 0, 2);
			int fileNameLength = bytesToInt(temp);
			curPos += 2;// 文件名称开始位
			temp = new byte[fileNameLength];
			System.arraycopy(ceb, curPos, temp, 0, fileNameLength);
			oMiFile[5] = new String(temp, "UTF8");
			miFileList.add(oMiFile);
			curPos += fileNameLength;
		}
		// ////////////////////////////////////////////////////////////////////////////
		// 数据区段索引表内数据区引用数据获取
		temp = new byte[4];
		curPos = DATA_INDEX_BEGIN;
		System.arraycopy(ceb, curPos, temp, 0, 4);
		curPos = bytesToInt(temp);// 数据区段索引表起始位
		temp = new byte[2];
		System.arraycopy(ceb, curPos, temp, 0, 2);
		int dataAreaNum = bytesToInt(temp);// 数据区段总个数
		curPos += 2;// 索引表长度起始位
		temp = new byte[2];
		System.arraycopy(ceb, curPos, temp, 0, 2);
		int aesDataIndexLength = bytesToInt(temp);// 索引表加密后的长度
		curPos += 2;// 索引表起始位
		temp = new byte[aesDataIndexLength];
		System.arraycopy(ceb, curPos, temp, 0, aesDataIndexLength);
		// 索引表解密
		byte[] tmp = AESCodec.decrypt(temp, aesKey);// decryptSymmetry("AES",
		                                            // aesKey, temp, null);
		if (tmp != null) temp = tmp;
		// ///////////////////////////////////////////////////////这里直接把解密后长度变化了的区段索引表数组塞回去
		// ///////////////////////////////////////////////////////start
		int cebLength = ceb.length - aesDataIndexLength + temp.length;
		byte[] deAesCeb = new byte[cebLength];
		System.arraycopy(ceb, 0, deAesCeb, 0, curPos);
		System.arraycopy(temp, 0, deAesCeb, curPos, temp.length);
		System.arraycopy(ceb, curPos + aesDataIndexLength, deAesCeb, curPos + temp.length, ceb.length - curPos - aesDataIndexLength);
		ceb = deAesCeb;
		// ///////////////////////////////////////////////////////end
		int idPos = curPos;// 段内id
		int pPos = curPos + 2;// 区段起始位置(绝对偏移)
		int lenPos = curPos + 6;// 区段长度
		// /////////////////////////////////////////////////////////////////////////////
		// 数据区段文件引用数据获取
		List<Object[]> dataFileList = new ArrayList<Object[]>();// 存储[区段id,文件id,文件是否压缩,压缩类型,文件起始位置(绝对偏移),文件长度,文件名称]列表
		for (int i = 0; i < dataAreaNum; i++)
		{
			curPos = idPos + i * 10;
			temp = new byte[2];
			System.arraycopy(ceb, curPos, temp, 0, 2);
			int id = bytesToInt(temp);// 区段id
			curPos += 2;// 区段起始位置开始位
			temp = new byte[4];
			System.arraycopy(ceb, curPos, temp, 0, 4);
			int p = bytesToInt(temp);
			curPos += 4;// 区段长度开始位
			temp = new byte[4];
			System.arraycopy(ceb, curPos, temp, 0, 4);
			int length = bytesToInt(temp);
			// ///////////////////////////////////////////////////
			curPos = p;// 本数据区段起始位置
			temp = new byte[2];
			System.arraycopy(ceb, curPos, temp, 0, 2);
			int areaId = bytesToInt(temp);
			curPos += 2;// 段内文件个数起始位置
			temp = new byte[2];
			System.arraycopy(ceb, curPos, temp, 0, 2);
			int fileNum = bytesToInt(temp);
			curPos += 2;// 索引表长度起始位
			temp = new byte[2];
			System.arraycopy(ceb, curPos, temp, 0, 2);
			aesDataIndexLength = bytesToInt(temp);// 索引表加密后的长度
			curPos += 2;// 索引表起始位
			temp = new byte[aesDataIndexLength];
			System.arraycopy(ceb, curPos, temp, 0, aesDataIndexLength);
			// 索引表解密
			int oLength = temp.length;
			tmp = AESCodec.decrypt(temp, aesKey);
			if (tmp != null) temp = tmp;
			// ///////////////////////////////////////////////////////这里直接把解密后长度变化了的文件索引表数组塞回去
			// ///////////////////////////////////////////////////////start
			int changeLength = temp.length - oLength;
			cebLength = ceb.length - aesDataIndexLength + temp.length;
			deAesCeb = new byte[cebLength];
			System.arraycopy(ceb, 0, deAesCeb, 0, curPos);
			System.arraycopy(temp, 0, deAesCeb, curPos, temp.length);
			System.arraycopy(ceb, curPos + aesDataIndexLength, deAesCeb, curPos + temp.length, ceb.length - curPos - aesDataIndexLength);
			ceb = deAesCeb;
			// ///////////////////////////////////////////////////////end
			// 文件id起始位置
			for (int j = 0; j < fileNum; j++)
			{
				Object[] oDataFile = new Object[8];// 存储[区段id,文件id,文件是否压缩,压缩类型,文件起始位置(绝对偏移),文件长度,文件名称,是否收费]
				oDataFile[0] = areaId;// 区段id
				temp = new byte[2];
				System.arraycopy(ceb, curPos, temp, 0, 2);
				oDataFile[1] = bytesToInt(temp);// 文件id
				curPos += 2;// 文件压缩标识开始位
				temp = new byte[2];
				System.arraycopy(ceb, curPos, temp, 0, 2);
				oDataFile[2] = (new Integer(temp[0])) == 0 ? false : true;// 文件是否压缩
				oDataFile[3] = new Integer(temp[1]);// 文件压缩类型，默认为0
				// ////////////////
				temp = new byte[1];
				System.arraycopy(ceb, curPos + 2, temp, 0, 1);
				oDataFile[7] = bytesToInt(temp);// 是否计费
				// //////////////////////
				curPos += 27;// 文件起始位置数据起始位置
				temp = new byte[4];
				System.arraycopy(ceb, curPos, temp, 0, 4);
				oDataFile[4] = p + bytesToInt(temp);
				curPos += 4;// 文件长度起始位置
				temp = new byte[4];
				System.arraycopy(ceb, curPos, temp, 0, 4);
				oDataFile[5] = bytesToInt(temp);
				curPos += 4;// 文件名称长度起始位置
				temp = new byte[2];
				System.arraycopy(ceb, curPos, temp, 0, 2);
				int fileNameLength = bytesToInt(temp);
				curPos += 2;// 文件名称开始位
				temp = new byte[fileNameLength];
				System.arraycopy(ceb, curPos, temp, 0, fileNameLength);
				oDataFile[6] = new String(temp, "UTF8");
				curPos += fileNameLength;
				dataFileList.add(oDataFile);
			}
			int pos = lenPos + i * 10;// 长度置换
			temp = intToBytes(length + changeLength, 4);
			System.arraycopy(temp, 0, ceb, pos, 4);
			// 以下因把解密后索引表直接塞进去而进行偏移置换
			if (i != dataAreaNum - 1)
			{
				pos = pPos + (i + 1) * 10;
				temp = intToBytes(p + length + changeLength, 4);
				System.arraycopy(temp, 0, ceb, pos, 4);
			}
		}
		
		//将解压的结果设回到cebfile中
		targetFile.setContents(ceb);
		targetFile.setMiFileList(miFileList);
		targetFile.setDataFileList(dataFileList);
		
//		for (int i = 0; i < miFileList.size(); i++)
//		{
//			Object[] objects = miFileList.get(i);
//			
//			String fileName = (String) objects[5];
//			temp = getFileDataFormList(fileName, FILE_TYPE_MI, miFileList, ceb, null);
//			if (temp != null) writeFile(folder + FILE_TYPE_MI + "/" + fileName, temp);
//		}
//		
//		for (int i = 0; i < dataFileList.size(); i++)
//		{
//			Object[] objects = dataFileList.get(i);
//			
//			String fileName = (String) objects[6];
//			temp = getFileDataFormList(fileName, FILE_TYPE_DATA, dataFileList, ceb, null);
//			if (temp != null)
//			{
//				writeFile(folder + FILE_TYPE_DATA + fileName, temp);
//			}
//		}
	}
	
	/** 字节数组转int,低字节优先 */
	private int bytesToInt(byte[] bArray) {
		StringBuffer sb = new StringBuffer(bArray.length);
		for (int i = bArray.length - 1; i >= 0; i--)
		{
			String sTemp = Integer.toHexString(0xff & bArray[i]);
			if (sTemp.length() < 2) sb.append(0);
			sb.append(sTemp.toUpperCase());
		}
		return Integer.parseInt(sb.toString(), 16);
	}
	
	/**
	 * int转字节数组,低字节优先
	 * 
	 * @param num
	 *            转换的数字
	 * @param length
	 *            转换成的字节数组长度
	 */
	private byte[] intToBytes(int num, int length) {
		String s = String.format("%08x", num).toUpperCase();
		byte[] bArray = new byte[length];
		char[] hexChars = s.toCharArray();
		for (int i = 0; i < length; i++)
		{
			int pos = (s.length() / 2 - i - 1) * 2;
			bArray[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1]));
		}
		return bArray;
	}
	
	/** char转byte */
	private byte charToByte(char c) {
		return (byte) "0123456789ABCDEF".indexOf(c);
	}
	
	/**
	 * 16位数组数据简单转换算法
	 * 
	 * @param before
	 *            扩展字段32字节数组,使用其中前8字节
	 */
	private byte[] changeBytes(byte[] before) {
		byte[] after = new byte[16];
		for (int i = 0; i < 8; i++)
		{
			after[i * 2] = before[i];
			after[i * 2 + 1] = before[7 - i];
		}
		return after;
	}
	
	/**
	 * 根据文件名从文件列表中获取文件字节信息
	 * 
	 * @param fileName
	 *            文件名
	 * @param fileType
	 *            文件类型:META-INF/DATA
	 * @param fileList
	 *            文件列表,ceb格式时为Object[],ocf格式时为byte[]
	 * @param ceb
	 *            ceb字节数据,ocf格式时为null
	 * @param fileNameList
	 *            文件名列表,ceb格式时此属性为null
	 * @return 文件字节信息,获取不到时返回null
	 */
	private byte[] getFileDataFormList(String fileName, String fileType, List fileList, byte[] ceb, List fileNameList) {
		if (FILE_TYPE_CEB.equals(bookFileType) && FILE_TYPE_MI.equals(fileType))
		{// [段内id,文件是否压缩,压缩类型,文件起始位置(绝对偏移),文件长度,文件名称]
			fileName = fileName.substring(fileName.lastIndexOf("/") + 1);
			for (int i = 0; i < fileList.size(); i++)
			{
				if (fileName.equals((String) ((Object[]) fileList.get(i))[5]))
				{
					int fileLength = ((Integer) ((Object[]) fileList.get(i))[4]).intValue();// 文件长度
					int startPos = ((Integer) ((Object[]) fileList.get(i))[3]).intValue();// 文件起始位置
					byte[] temp = new byte[fileLength];
					System.arraycopy(ceb, startPos, temp, 0, fileLength);
					return temp;
				}
			}
			return null;
		}
		else if (FILE_TYPE_CEB.equals(bookFileType) && FILE_TYPE_DATA.equals(fileType))
		{// [区段id,文件id,文件是否压缩,压缩类型,文件起始位置(绝对偏移),文件长度,文件名称]
			for (int i = 0; i < fileList.size(); i++)
			{
				if (fileName.equals((String) ((Object[]) fileList.get(i))[6]))
				{
					int fileLength = ((Integer) ((Object[]) fileList.get(i))[5]).intValue();// 文件长度
					int startPos = ((Integer) ((Object[]) fileList.get(i))[4]).intValue();// 文件起始位置
					byte[] temp = new byte[fileLength];
					System.arraycopy(ceb, startPos, temp, 0, fileLength);
					
					return temp;
				}
			}
			return null;
		}
		else return null;
	}
	
	private void writeFile(String filePath, byte[] b) throws Exception {
		File file = new File(filePath.substring(0, filePath.lastIndexOf("/")));
		if (!file.exists()) file.mkdirs();
		
		ByteArrayInputStream bis = new ByteArrayInputStream(b);
		FileOutputStream fos = new FileOutputStream(filePath);
		byte[] buffer = new byte[1024];
		int len = 0;
		while ((len = bis.read(buffer)) > 0)
		{
			fos.write(buffer, 0, len);
		}
		bis.close();
		fos.close();
	}
	
}
